﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VoxelConverter.Util;

namespace VoxelConverter
{
    public class VoxelToObj
    {
        /// <summary>
        /// Converts a VoxelModel into a Wavefront file string
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        public static string Convert(VoxelModel model)
        {
            if (model == null)
            {
                Console.WriteLine("ERROR: Null Model");
                return string.Empty;
            }

            // Create Stringbuilder for creating OBJ file
            StringBuilder sb = new StringBuilder();

            Console.WriteLine("Creating OBJ File...");

            // Create File Header
            sb.AppendLine("# Voxel-to-Wavefront");
            sb.AppendLine("# Created by Black Unicorn Games");
            sb.AppendLine();

            Console.WriteLine("Appending Normal Coordinates...");

            // Create Vertex Normals
            sb.AppendLine("# Vertex Normals - 6 total");
            sb.AppendLine("vn  1  0  0");
            sb.AppendLine("vn -1  0  0");
            sb.AppendLine("vn  0  1  0");
            sb.AppendLine("vn  0 -1  0");
            sb.AppendLine("vn  0  0  1");
            sb.AppendLine("vn  0  0 -1");
            sb.AppendLine();

            // Create the Texture Coordinates
            Dictionary<int, TextureQuad> texQuads = AppendTextureCoords(sb, model);

            // Create the Quads
            AppendQuads(sb, model, texQuads);

            // Return the contents
            return sb.ToString();
        }

        private static Dictionary<int, TextureQuad> AppendTextureCoords(StringBuilder sb, VoxelModel model)
        {
            Console.WriteLine("Creating Texture Coordinates...");

            Dictionary<int, TextureQuad> quadDict = new Dictionary<int, TextureQuad>(model.VoxelIndices.Length);

            // Create Vertex Texture Coords
            sb.AppendLine("# Vertex Texture Coordinates");

            // Assume a 16x16 texture atlas
            //  assumption based on, well, because I can
            for (int i = 0; i < model.VoxelIndices.Length; i++)
            {
                // Calculate the Integer Array positions of the Index on a 16x16 grid
                int index = model.VoxelIndices[i];
                int x = index / 16;
                int y = index % 16;

                // Create each Texture coordinate for a quad of 1/16x1/16 dimensions
                string format = "vt {0} {1}\n";
                sb.AppendFormat(format, ((float)x / 16f).ToString("f4"), ((float)y / 16f).ToString("f4"));
                sb.AppendFormat(format, ((float)(x + 1) / 16f).ToString("f4"), ((float)y / 16f).ToString("f4"));
                sb.AppendFormat(format, ((float)x / 16f).ToString("f4"), ((float)(y + 1) / 16f).ToString("f4"));
                sb.AppendFormat(format, ((float)(x + 1) / 16f).ToString("f4"), ((float)(y + 1) / 16f).ToString("f4"));

                // Add the texture quad to the dictionary
                TextureQuad quad = new TextureQuad((i * 4) + 1, (i * 4) + 2, (i * 4) + 3, (i * 4) + 4);
                quadDict.Add(index, quad);
            }

            // Append blank line
            sb.AppendLine();

            // Return the Quad Dictionary for use when creating faces
            return quadDict;
        }

        private static void AppendQuads(StringBuilder sb, VoxelModel model, Dictionary<int, TextureQuad> texQuads)
        {
            // Append header
            sb.AppendLine("# Vertices and Faces");
            sb.AppendLine();

            // Create the Quads
            foreach (SweepDirection direction in Enum.GetValues(typeof(SweepDirection)))
            {
                GenerateOptimizedQuads(sb, model, texQuads, direction);
            }
        }

        private static void GenerateOptimizedQuads(StringBuilder sb, VoxelModel model, Dictionary<int, TextureQuad> texQuads, SweepDirection direction)
        {
            // Create Pointers to Ints...essentially
            //  Without writing 'unsafe' code and dealing with fixing positions in memory
            //  this is the best way to go about it imo. 
            #region
            Ref<int> x = new Ref<int>(0);
            Ref<int> y = new Ref<int>(0);
            Ref<int> z = new Ref<int>(0);
            Ref<int> width;
            Ref<int> height;
            Ref<int> depth;
            int widthMax = 0;
            int heightMax = 0;
            int depthMax = 0;
            int depthCheck = ((int)direction) % 2 == 0 ? 1 : -1; // used for checking depth-wise adjacent voxels
            #endregion

            // Setup the Height/Width/Depth for the loops
            #region
            switch (direction)
            {
                case SweepDirection.XNegative:
                case SweepDirection.XPositive:
                    depth = x;
                    width = y;
                    height = z;
                    depthMax = model.Width;
                    widthMax = model.Height;
                    heightMax = model.Depth;
                    break;
                case SweepDirection.YNegative:
                case SweepDirection.YPositive:
                    depth = y;
                    width = z;
                    height = x;
                    depthMax = model.Height;
                    widthMax = model.Depth;
                    heightMax = model.Width;
                    break;
                case SweepDirection.ZNegative:
                case SweepDirection.ZPositive:
                    depth = z;
                    width = x;
                    height = y;
                    depthMax = model.Depth;
                    widthMax = model.Width;
                    heightMax = model.Height;
                    break;
                default:
                    depth = z;
                    width = x;
                    height = y;
                    break;
            }
            #endregion

            for (depth.Value = 0; depth.Value < depthMax; depth.Value++)
            {
                List<VoxelQuad> sliceQuads = new List<VoxelQuad>();

                for (height.Value = 0; height.Value < heightMax; height.Value++)
                {
                    List<VoxelQuad> rowQuads = new List<VoxelQuad>();

                    int rowQuadIndex = 0;

                    for (width.Value = 0; width.Value < widthMax; width.Value++)
                    {
                        // Get the current voxel index
                        // Check to see if empty, if so then skip
                        int currentIndex = x.Value + (y.Value * model.Width) + (z.Value * model.Width * model.Height);
                        if (model.VoxelData[currentIndex] < 0)
                            continue;

                        // Get the depth-wise adjacent voxel index
                        // Reset depth value
                        depth.Value += depthCheck;
                        int depthIndex = x.Value + (y.Value * model.Width) + (z.Value * model.Width * model.Height);
                        depth.Value -= depthCheck;

                        // IFF the adjacent depth-wise voxel is not empty
                        // THEN skip this voxel for processing
                        if (depth.Value == 0 && depthCheck == 1 && model.VoxelData[depthIndex] >= 0)
                            continue;
                        if (depth.Value == depthMax - 1 && depthCheck == -1 && model.VoxelData[depthIndex] >= 0)
                            continue;
                        else if (depth.Value > 0 && depth.Value < depthMax - 1 && model.VoxelData[depthIndex] >= 0)
                            continue;

                        // Process the voxel
                        #region

                        if (rowQuads.Count <= 0)
                        {
                            rowQuads.Add(new VoxelQuad()
                            {
                                Height = 1,
                                Width = 1,
                                X = width.Value,
                                Y = height.Value,
                                Type = model.VoxelData[currentIndex]
                            });

                            rowQuadIndex = 0;
                        }
                        else
                        {
                            if (rowQuads[rowQuadIndex].X + rowQuads[rowQuadIndex].Width == width.Value &&
                                rowQuads[rowQuadIndex].Type == model.VoxelData[currentIndex])
                                rowQuads[rowQuadIndex].Width += 1;
                            else
                            {
                                rowQuads.Add(new VoxelQuad()
                                {
                                    Height = 1,
                                    Width = 1,
                                    X = width.Value,
                                    Y = height.Value,
                                    Type = model.VoxelData[currentIndex]
                                });

                                // Update the rowQuad index to keep looking at the current one
                                // --could use .first or .top instead
                                rowQuadIndex++;
                            }
                        }

                        #endregion
                    }

                    // Process the Row
                    #region

                    for (int i = 0; i < rowQuads.Count; i++)
                    {
                        VoxelQuad rowQuad = rowQuads[i];

                        if (sliceQuads.Count > 0)
                        {
                            List<VoxelQuad> sliceTemp = sliceQuads.Where(q => q.Type == rowQuad.Type
                                                                              && (q.Y + q.Height) == rowQuad.Y
                                                                              && q.X == rowQuad.X).ToList();
                            if (sliceTemp.Count != 1)
                                sliceQuads.Add(rowQuad); // Add the quad as there shouldn't be more than one match
                            else
                            {
                                if (sliceTemp[0].Width < rowQuad.Width)
                                {
                                    // Update the existing quad
                                    // Update the current Row Quad
                                    // Add the row quad to the slice quads
                                    sliceTemp[0].Height += 1;
                                    rowQuad.Width -= sliceTemp[0].Width;
                                    rowQuad.X += sliceTemp[0].Width;
                                    sliceQuads.Add(rowQuad);
                                }
                                else if (sliceTemp[0].Width == rowQuad.Width)
                                {
                                    // Update the existing quad
                                    sliceTemp[0].Height += 1;
                                }
                                else
                                {
                                    // Add the current Row Quad to the slice quads
                                    sliceQuads.Add(rowQuad);
                                }
                            }
                        }
                        else
                        {
                            // Go ahead and add the quad to the slice quads
                            sliceQuads.Add(rowQuad);
                        }
                    }

                    #endregion
                }

                // Process the slice
                ProcessSliceQuads(sliceQuads, depth.Value, direction, sb, texQuads);
            }
        }

        private static void ProcessSliceQuads(List<VoxelQuad> slice, int sliceDepth, SweepDirection direction, StringBuilder sb, Dictionary<int, TextureQuad> texQuads)
        {
            // Create Header
            sb.AppendLine("# Slice Processing:");
            sb.AppendFormat("# * Direction = {0}\n", direction);
            sb.AppendFormat("# * Slice = {0}\n", sliceDepth);
            sb.AppendLine();

            // Create Pointers to Ints...essentially
            //  Without writing 'unsafe' code and dealing with fixing positions in memory
            //  this is the best way to go about it imo. 
            #region
            Ref<int> x = new Ref<int>(0);
            Ref<int> y = new Ref<int>(0);
            Ref<int> z = new Ref<int>(0);
            Ref<int> width;
            Ref<int> height;
            Ref<int> depth;
            int normalIndex = ((int)direction) + 1;
            #endregion

            // Setup the Height/Width/Depth for the loop
            #region
            switch (direction)
            {
                case SweepDirection.XNegative:
                case SweepDirection.XPositive:
                    depth = x;
                    width = y;
                    height = z;
                    break;
                case SweepDirection.YNegative:
                case SweepDirection.YPositive:
                    depth = y;
                    width = z;
                    height = x;
                    break;
                case SweepDirection.ZNegative:
                case SweepDirection.ZPositive:
                    depth = z;
                    width = x;
                    height = y;
                    break;
                default:
                    depth = z;
                    width = x;
                    height = y;
                    break;
            }
            #endregion

            depth.Value = sliceDepth;
            depth.Value += ((int)direction) % 2 == 0 ? 1 : 0; // Set the offset based on direction

            foreach (VoxelQuad quad in slice)
            {
                // Top Left
                width.Value = quad.X;
                height.Value = quad.Y;
                Vector3D bottomLeft = new Vector3D(x.Value, y.Value, z.Value);

                // Top Right
                width.Value = quad.X + quad.Width;
                Vector3D bottomRight = new Vector3D(x.Value, y.Value, z.Value);

                // Bottom Right
                height.Value = quad.Y + quad.Height;
                Vector3D topRight = new Vector3D(x.Value, y.Value, z.Value);

                // Bottom Left
                width.Value = quad.X;
                Vector3D topLeft = new Vector3D(x.Value, y.Value, z.Value);

                // Append Face Vertices
                string vectorFormat = "v {0:f4} {1:f4} {2:f4}\n";
                sb.AppendFormat(vectorFormat, topLeft.X, topLeft.Y, topLeft.Z);
                sb.AppendFormat(vectorFormat, topRight.X, topRight.Y, topRight.Z);
                sb.AppendFormat(vectorFormat, bottomRight.X, bottomRight.Y, bottomRight.Z);
                sb.AppendFormat(vectorFormat, bottomLeft.X, bottomLeft.Y, bottomLeft.Z);

                // Get Texture Quad mapping
                TextureQuad texQuad = texQuads.First().Value;
                if (texQuads.ContainsKey(quad.Type))
                    texQuad = texQuads[quad.Type];

                // Append face to file
                string faceFormat = "{0}/{2}/{1}";
                sb.Append("f ");
                sb.AppendFormat(faceFormat + " ", -1, normalIndex, texQuad.BottomLeft);
                sb.AppendFormat(faceFormat + " ", -2, normalIndex, texQuad.BottomRight);
                sb.AppendFormat(faceFormat + " ", -3, normalIndex, texQuad.TopRight);
                sb.AppendFormat(faceFormat, -4, normalIndex, texQuad.TopLeft);
                sb.AppendLine();
                sb.AppendLine();

            }
        }
    }

    public struct TextureQuad
    {
        public int TopLeft;
        public int TopRight;
        public int BottomLeft;
        public int BottomRight;

        public TextureQuad(int tl, int tr, int bl, int br)
        {
            TopLeft = tl;
            TopRight = tr;
            BottomLeft = bl;
            BottomRight = br;
        }
    }

    public class VoxelQuad
    {
        public int Width = 0;
        public int Height = 0;
        public int X = 0;
        public int Y = 0;
        public int Type = -1;
    }

    public struct Vector3D
    {
        public float X;
        public float Y;
        public float Z;

        public Vector3D(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }
}
